package core.util;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.text.Normalizer;
import java.text.Normalizer.Form;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Map.Entry;

/**
 * 文字関連クラス
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class MojiUtil {

	/** エンコード */
	public static final Charset CHARSET_JIS = Charset.forName("iso-2022-jp");
	/** エンコード */
	public static final Charset CHARSET_XJIS = Charset.forName("x-windows-iso2022jp");
	/** エンコード */
	public static final Charset CHARSET_W31J = Charset.forName("Windows-31J");
	/** エンコード */
	public static final Charset CHARSET_SJIS = Charset.forName("SJIS");
	/** エンコード */
	public static final Charset CHARSET_EUC = Charset.forName("euc-jp");

	/** 改行コード */
	public static final String RET_CODE_CR = "\r";
	/** 改行コード */
	public static final String RET_CODE_LF = "\n";
	/** 改行コード */
	public static final String RET_CODE_CRLF = "\r\n";

	/** UTF-8用 BOM */
	private static final byte[] UTF8_BOM = {(byte)0xEF, (byte)0xBB, (byte)0xBF};

	/** 半角文字をキーにしたHashMap */
	private static final Map<Integer, String> H2F_TABLE;
	/** 全角文字をキーにしたHashMap */
	private static final Map<Integer, String> F2H_TABLE;

	/**
	 * 大文字、小文字それぞれをキーにした文字一覧を生成する。
	 *
	 */
	static {
		// 全角→半角 用テーブル
		Map<Integer, String> f2h = new HashMap<>();
		f2h.put(Integer.valueOf('１'), "1");
		f2h.put(Integer.valueOf('２'), "2");
		f2h.put(Integer.valueOf('３'), "3");
		f2h.put(Integer.valueOf('４'), "4");
		f2h.put(Integer.valueOf('５'), "5");
		f2h.put(Integer.valueOf('６'), "6");
		f2h.put(Integer.valueOf('７'), "7");
		f2h.put(Integer.valueOf('８'), "8");
		f2h.put(Integer.valueOf('９'), "9");
		f2h.put(Integer.valueOf('０'), "0");

		f2h.put(Integer.valueOf('Ａ'), "A");
		f2h.put(Integer.valueOf('Ｂ'), "B");
		f2h.put(Integer.valueOf('Ｃ'), "C");
		f2h.put(Integer.valueOf('Ｄ'), "D");
		f2h.put(Integer.valueOf('Ｅ'), "E");
		f2h.put(Integer.valueOf('Ｆ'), "F");
		f2h.put(Integer.valueOf('Ｇ'), "G");
		f2h.put(Integer.valueOf('Ｈ'), "H");
		f2h.put(Integer.valueOf('Ｉ'), "I");
		f2h.put(Integer.valueOf('Ｊ'), "J");
		f2h.put(Integer.valueOf('Ｋ'), "K");
		f2h.put(Integer.valueOf('Ｌ'), "L");
		f2h.put(Integer.valueOf('Ｍ'), "M");
		f2h.put(Integer.valueOf('Ｎ'), "N");
		f2h.put(Integer.valueOf('Ｏ'), "O");
		f2h.put(Integer.valueOf('Ｐ'), "P");
		f2h.put(Integer.valueOf('Ｑ'), "Q");
		f2h.put(Integer.valueOf('Ｒ'), "R");
		f2h.put(Integer.valueOf('Ｓ'), "S");
		f2h.put(Integer.valueOf('Ｔ'), "T");
		f2h.put(Integer.valueOf('Ｕ'), "U");
		f2h.put(Integer.valueOf('Ｖ'), "V");
		f2h.put(Integer.valueOf('Ｗ'), "W");
		f2h.put(Integer.valueOf('Ｘ'), "X");
		f2h.put(Integer.valueOf('Ｙ'), "Y");
		f2h.put(Integer.valueOf('Ｚ'), "Z");

		f2h.put(Integer.valueOf('ａ'), "a");
		f2h.put(Integer.valueOf('ｂ'), "b");
		f2h.put(Integer.valueOf('ｃ'), "c");
		f2h.put(Integer.valueOf('ｄ'), "d");
		f2h.put(Integer.valueOf('ｅ'), "e");
		f2h.put(Integer.valueOf('ｆ'), "f");
		f2h.put(Integer.valueOf('ｇ'), "g");
		f2h.put(Integer.valueOf('ｈ'), "h");
		f2h.put(Integer.valueOf('ｉ'), "i");
		f2h.put(Integer.valueOf('ｊ'), "j");
		f2h.put(Integer.valueOf('ｋ'), "k");
		f2h.put(Integer.valueOf('ｌ'), "l");
		f2h.put(Integer.valueOf('ｍ'), "m");
		f2h.put(Integer.valueOf('ｎ'), "n");
		f2h.put(Integer.valueOf('ｏ'), "o");
		f2h.put(Integer.valueOf('ｐ'), "p");
		f2h.put(Integer.valueOf('ｑ'), "q");
		f2h.put(Integer.valueOf('ｒ'), "r");
		f2h.put(Integer.valueOf('ｓ'), "s");
		f2h.put(Integer.valueOf('ｔ'), "t");
		f2h.put(Integer.valueOf('ｕ'), "u");
		f2h.put(Integer.valueOf('ｖ'), "v");
		f2h.put(Integer.valueOf('ｗ'), "w");
		f2h.put(Integer.valueOf('ｘ'), "x");
		f2h.put(Integer.valueOf('ｙ'), "y");
		f2h.put(Integer.valueOf('ｚ'), "z");

		f2h.put(Integer.valueOf('ア'), "ｱ");
		f2h.put(Integer.valueOf('イ'), "ｲ");
		f2h.put(Integer.valueOf('ウ'), "ｳ");
		f2h.put(Integer.valueOf('エ'), "ｴ");
		f2h.put(Integer.valueOf('オ'), "ｵ");
		f2h.put(Integer.valueOf('カ'), "ｶ");
		f2h.put(Integer.valueOf('キ'), "ｷ");
		f2h.put(Integer.valueOf('ク'), "ｸ");
		f2h.put(Integer.valueOf('ケ'), "ｹ");
		f2h.put(Integer.valueOf('コ'), "ｺ");
		f2h.put(Integer.valueOf('サ'), "ｻ");
		f2h.put(Integer.valueOf('シ'), "ｼ");
		f2h.put(Integer.valueOf('ス'), "ｽ");
		f2h.put(Integer.valueOf('セ'), "ｾ");
		f2h.put(Integer.valueOf('ソ'), "ｿ");
		f2h.put(Integer.valueOf('タ'), "ﾀ");
		f2h.put(Integer.valueOf('チ'), "ﾁ");
		f2h.put(Integer.valueOf('ツ'), "ﾂ");
		f2h.put(Integer.valueOf('テ'), "ﾃ");
		f2h.put(Integer.valueOf('ト'), "ﾄ");
		f2h.put(Integer.valueOf('ナ'), "ﾅ");
		f2h.put(Integer.valueOf('ニ'), "ﾆ");
		f2h.put(Integer.valueOf('ヌ'), "ﾇ");
		f2h.put(Integer.valueOf('ネ'), "ﾈ");
		f2h.put(Integer.valueOf('ノ'), "ﾉ");
		f2h.put(Integer.valueOf('ハ'), "ﾊ");
		f2h.put(Integer.valueOf('ヒ'), "ﾋ");
		f2h.put(Integer.valueOf('フ'), "ﾌ");
		f2h.put(Integer.valueOf('ヘ'), "ﾍ");
		f2h.put(Integer.valueOf('ホ'), "ﾎ");
		f2h.put(Integer.valueOf('マ'), "ﾏ");
		f2h.put(Integer.valueOf('ミ'), "ﾐ");
		f2h.put(Integer.valueOf('ム'), "ﾑ");
		f2h.put(Integer.valueOf('メ'), "ﾒ");
		f2h.put(Integer.valueOf('モ'), "ﾓ");
		f2h.put(Integer.valueOf('ヤ'), "ﾔ");
		f2h.put(Integer.valueOf('ユ'), "ﾕ");
		f2h.put(Integer.valueOf('ヨ'), "ﾖ");
		f2h.put(Integer.valueOf('ラ'), "ﾗ");
		f2h.put(Integer.valueOf('リ'), "ﾘ");
		f2h.put(Integer.valueOf('ル'), "ﾙ");
		f2h.put(Integer.valueOf('レ'), "ﾚ");
		f2h.put(Integer.valueOf('ロ'), "ﾛ");
		f2h.put(Integer.valueOf('ワ'), "ﾜ");
		f2h.put(Integer.valueOf('ヲ'), "ｦ");
		f2h.put(Integer.valueOf('ン'), "ﾝ");

		f2h.put(Integer.valueOf('ァ'), "ｧ");
		f2h.put(Integer.valueOf('ィ'), "ｨ");
		f2h.put(Integer.valueOf('ゥ'), "ｩ");
		f2h.put(Integer.valueOf('ェ'), "ｪ");
		f2h.put(Integer.valueOf('ォ'), "ｫ");
		f2h.put(Integer.valueOf('ッ'), "ｯ");
		f2h.put(Integer.valueOf('ャ'), "ｬ");
		f2h.put(Integer.valueOf('ュ'), "ｭ");
		f2h.put(Integer.valueOf('ョ'), "ｮ");

		f2h.put(Integer.valueOf('゛'), "ﾞ");
		f2h.put(Integer.valueOf('゜'), "ﾟ");
		f2h.put(Integer.valueOf('、'), "､");
		f2h.put(Integer.valueOf('。'), "｡");
		f2h.put(Integer.valueOf('・'), "･");
		f2h.put(Integer.valueOf('ー'), "ｰ");
		f2h.put(Integer.valueOf('「'), "｢");
		f2h.put(Integer.valueOf('」'), "｣");
		f2h.put(Integer.valueOf('！'), "!");
		f2h.put(Integer.valueOf('”'), "\"");
		f2h.put(Integer.valueOf('＃'), "#");
		f2h.put(Integer.valueOf('＄'), "$");
		f2h.put(Integer.valueOf('％'), "%");
		f2h.put(Integer.valueOf('＆'), "&");
		f2h.put(Integer.valueOf('’'), "'");
		f2h.put(Integer.valueOf('（'), "(");
		f2h.put(Integer.valueOf('）'), ")");
		f2h.put(Integer.valueOf('＊'), "*");
		f2h.put(Integer.valueOf('＋'), "+");
		f2h.put(Integer.valueOf('，'), ",");
		f2h.put(Integer.valueOf('．'), ".");
		f2h.put(Integer.valueOf('／'), "/");
		f2h.put(Integer.valueOf('；'), ";");
		f2h.put(Integer.valueOf('：'), ":");
		f2h.put(Integer.valueOf('＜'), "<");
		f2h.put(Integer.valueOf('＝'), "=");
		f2h.put(Integer.valueOf('＞'), ">");
		f2h.put(Integer.valueOf('？'), "?");
		f2h.put(Integer.valueOf('＠'), "@");
		f2h.put(Integer.valueOf('［'), "[");
		f2h.put(Integer.valueOf('￥'), "\\");
		f2h.put(Integer.valueOf('］'), "]");
		f2h.put(Integer.valueOf('＾'), "^");
		f2h.put(Integer.valueOf('＿'), "_");
		f2h.put(Integer.valueOf('｀'), "`");
		f2h.put(Integer.valueOf('｛'), "{");
		f2h.put(Integer.valueOf('｜'), "|");
		f2h.put(Integer.valueOf('｝'), "}");
		f2h.put(Integer.valueOf('　'), " ");

		// 半角→全角 用テーブル
		Map<Integer, String> h2f = new HashMap<>();
		for (final Entry<Integer, String> me : f2h.entrySet()) {
			String zkey = String.valueOf(Character.toChars(me.getKey().intValue()));
			h2f.put(Integer.valueOf(me.getValue().codePointAt(0)), zkey);
		}

		// 全角→半角 用テーブル
		f2h.put(Integer.valueOf('ヴ'), "ｳﾞ");
		f2h.put(Integer.valueOf('ガ'), "ｶﾞ");
		f2h.put(Integer.valueOf('ギ'), "ｷﾞ");
		f2h.put(Integer.valueOf('グ'), "ｸﾞ");
		f2h.put(Integer.valueOf('ゲ'), "ｹﾞ");
		f2h.put(Integer.valueOf('ゴ'), "ｺﾞ");
		f2h.put(Integer.valueOf('ザ'), "ｻﾞ");
		f2h.put(Integer.valueOf('ジ'), "ｼﾞ");
		f2h.put(Integer.valueOf('ズ'), "ｽﾞ");
		f2h.put(Integer.valueOf('ゼ'), "ｾﾞ");
		f2h.put(Integer.valueOf('ゾ'), "ｿﾞ");
		f2h.put(Integer.valueOf('ダ'), "ﾀﾞ");
		f2h.put(Integer.valueOf('ヂ'), "ﾁﾞ");
		f2h.put(Integer.valueOf('ヅ'), "ﾂﾞ");
		f2h.put(Integer.valueOf('デ'), "ﾃﾞ");
		f2h.put(Integer.valueOf('ド'), "ﾄﾞ");
		f2h.put(Integer.valueOf('バ'), "ﾊﾞ");
		f2h.put(Integer.valueOf('ブ'), "ﾌﾞ");
		f2h.put(Integer.valueOf('ビ'), "ﾋﾞ");
		f2h.put(Integer.valueOf('ベ'), "ﾍﾞ");
		f2h.put(Integer.valueOf('ボ'), "ﾎﾞ");
		f2h.put(Integer.valueOf('パ'), "ﾊﾟ");
		f2h.put(Integer.valueOf('ピ'), "ﾋﾟ");
		f2h.put(Integer.valueOf('プ'), "ﾌﾟ");
		f2h.put(Integer.valueOf('ペ'), "ﾍﾟ");
		f2h.put(Integer.valueOf('ポ'), "ﾎﾟ");

		f2h.put(Integer.valueOf('ヵ'), "ｶ");
		f2h.put(Integer.valueOf('ヶ'), "ｹ");
		f2h.put(Integer.valueOf('ヮ'), "ﾜ");
		f2h.put(Integer.valueOf('ヰ'), "ｲ");
		f2h.put(Integer.valueOf('ヱ'), "ｴ");
		f2h.put(Integer.valueOf('ゎ'), "ﾜ");
		f2h.put(Integer.valueOf('ゐ'), "ｲ");
		f2h.put(Integer.valueOf('ゑ'), "ｴ");
		f2h.put(Integer.valueOf('\u2212'), "-");
		f2h.put(Integer.valueOf('\uff0d'), "-");

		F2H_TABLE = Collections.unmodifiableMap(f2h);
		H2F_TABLE = Collections.unmodifiableMap(h2f);
	}

	/**
	 * コンストラクタ
	 */
	private MojiUtil() {
		throw new AssertionError();
	}

	/**
	 * UTF-8BOM値取得
	 * @return UTF-8BOM値
	 */
	public static byte[] utf8Bom() {
		return Arrays.copyOf(UTF8_BOM, UTF8_BOM.length);
	}

	/**
	 * 対象の文字列内の変換できる文字だけ半角に変換する。
	 *
	 * @param item 対象文字列
	 * @param space true:スペースも変換する。false:スペースは変換しない。
	 * @param kana true:カナも変換する。false:カナは変換しない。
	 * @return 半角に変換した文字列
	 */
	public static String toHalfCase(final String item,
			final boolean space, final boolean kana) {
		StringBuilder newStr = null;
		for (int i = 0; item != null && i < item.length(); i = item.offsetByCodePoints(i, 1)) {
			int c = item.codePointAt(i);

			String str = null;
			if (c != '　' || space) {
				str = F2H_TABLE.get(Integer.valueOf(c));
				if (str != null && !kana) {
					int cp = str.codePointAt(0);
					if (0xff61 <= cp && cp <= 0xff9f) {
						str = null;
					}
				}
			}

			if (str != null) {
				if (newStr == null) {
					newStr = new StringBuilder(item.substring(0, i));
				}
				newStr.append(str);
			} else {
				if (newStr != null) {
					newStr.appendCodePoint(c);
				}
			}
		}

		return Objects.toString(newStr, Objects.toString(item, ""));
	}

	/**
	 * 対象の文字列内の変換できる文字だけを全角に変換する。
	 *
	 * @param item 対象文字列
	 * @param space true:スペースも変換する。false:スペースは変換しない。
	 * @return 全角に変換した文字列
	 */
	public static String toFullCase(final String item, final boolean space) {
		return toFullCase(item, space, CHARSET_W31J);
	}

	/**
	 * 対象の文字列内の変換できる文字だけを全角に変換する。
	 *
	 * @param item 対象文字列
	 * @param space true:スペースも変換する。false:スペースは変換しない。
	 * @param charset 変換後出力エンコード
	 * @return 全角に変換した文字列
	 */
	public static String toFullCase(final String item, final boolean space, final Charset charset) {
		String hyphen = String.valueOf(Character.toChars(correctGarbled('\uff0d', charset)));
		StringBuilder newStr = null;
		for (int i = 0; item != null && i < item.length(); i = item.offsetByCodePoints(i, 1)) {
			int c = item.codePointAt(i);

			String str = null;
			if (c == '-') {
				str = hyphen;
			} else if (c != ' ' || space) {
				int n = item.offsetByCodePoints(i, 1);
				if (n < item.length()) {
					str = toNormal(c, item.codePointAt(n));
					if (str != null) {
						newStr = addBuilder(newStr, item, i, str, c);
						i = n;
						continue;
					}
				}
				str = H2F_TABLE.get(Integer.valueOf(c));
			}

			newStr = addBuilder(newStr, item, i, str, c);
		}

		return Objects.toString(newStr, Objects.toString(item, ""));
	}

	/**
	 * 文字列付加
	 * @param newStr 文字列バッファ
	 * @param item 元文字列
	 * @param i 位置
	 * @param str 付加文字列
	 * @param c コードポイント
	 * @return 文字列バッファ
	 */
	private static StringBuilder addBuilder(final StringBuilder newStr, final String item,
			final int i, final String str, final int c) {
		if (str != null) {
			if (newStr == null) {
				StringBuilder sb = new StringBuilder(item.substring(0, i));
				sb.append(str);
				return sb;
			}
			newStr.append(str);
		} else {
			if (newStr != null) {
				newStr.appendCodePoint(c);
			}
		}
		return newStr;
	}

	/**
	 * 正規化
	 * @param cp コードポイント
	 * @return 正規化文字列
	 */
	private static String toNormal(final int... cp) {
		if (cp.length == 2 && (cp[1] == 'ﾞ' || cp[1] == 'ﾟ')) {
			String ret = Normalizer.normalize(Normalizer.normalize(
					new String(cp, 0, cp.length), Form.NFKD), Form.NFC);
			if (ret.length() == 1) {
				return ret;
			}
		}
		return null;
	}

	/**
	 * 文字化け対応(SJIS <-> Windows-31J)
	 *
	 * @param str 文字列
	 * @param charset 変換後出力エンコード
	 * @return 変換後文字列
	 */
	public static String correctGarbled(final String str, final Charset charset) {
		StringBuilder sb = null;
		if (charset != null && !isUtf(charset)) {
			for (int i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
				int cp = str.codePointAt(i);
				int ch = correctGarbled(cp, charset);
				if (ch != cp) {
					if (sb == null) {
						sb = new StringBuilder(str.substring(0, i));
					}
					sb.appendCodePoint(ch);
				} else {
					if (sb != null) {
						sb.appendCodePoint(ch);
					}
				}
			}
		}
		return Objects.toString(sb, str);
	}

	/**
	 * 文字化け対応(SJIS <-> Windows-31J)
	 *
	 * @param ch キャラクタ
	 * @param charset 変換後出力エンコード
	 * @return 変換後キャラクタ
	 */
	public static int correctGarbled(final int ch, final Charset charset) {
		int chr = ch;

		// Windows-31J ～ ∥ － ￠ ￡ ￢ ―
		if (ch == '\uff5e' || ch == '\u2225' || ch == '\uff0d'
				|| ch == '\uffe0' || ch == '\uffe1' || ch == '\uffe2' || ch == '\u2015') {
			if (charset != null
					&& (CHARSET_EUC.equals(charset) || CHARSET_JIS.equals(charset)
						|| CHARSET_SJIS.equals(charset) || "Shift_JIS".equals(charset.name()))) {
				byte[] b = String.valueOf(Character.toChars(ch)).getBytes(CHARSET_W31J);
				chr = new String(b, CHARSET_SJIS).codePointAt(0);
			}

		// Windows-31J以外
		} else if (ch == '\u301c' || ch == '\u2016' || ch == '\u2212'
				|| ch == '\u00a2' || ch == '\u00a3' || ch == '\u00ac' || ch == '\u2014') {
			if (charset != null && CHARSET_W31J.equals(charset)) {
				byte[] b = String.valueOf(Character.toChars(ch)).getBytes(CHARSET_SJIS);
				chr = new String(b, CHARSET_W31J).codePointAt(0);
			}
		}

		return chr;
	}

	/**
	 * UTF確認
	 *
	 * @param charset エンコード
	 * @return UTFの場合 true を返す。
	 */
	public static boolean isUtf(final Charset charset) {
		return StandardCharsets.UTF_8.equals(charset) || StandardCharsets.UTF_16BE.equals(charset)
				|| StandardCharsets.UTF_16LE.equals(charset);
	}

	/**
	 * 文字列がShift_JIS　第1水準・第2水準かをチェックする。
	 *
	 * @param str 判定対象文字列
	 * @param kana 半角カナを含む場合 true
	 * @param ext ユーザ定義領域を含む場合 true
	 * @return	対応文字列の場合はtrue 対象文字列以外の場合はfalse
	 */
	public static boolean isShiftJIS(final String str, final boolean kana, final boolean ext) {
		// 文字列分解
		for (int i = 0; str != null && i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (!isShiftJIS(str.codePointAt(i), kana, ext)) {
				return false;
			}
		}
		return true;
	}

	/**
	 * Shift_JIS　第1水準・第2水準かをチェックする。
	 *
	 * @param ch 判定対象文字
	 * @param kana 半角カナを含む場合 true
	 * @param ext 機種依存、拡張領域を含む場合 true
	 * @return	対応文字の場合はtrue	対応文字以外の場合はfalse
	 */
	public static boolean isShiftJIS(final int ch, final boolean kana, final boolean ext) {
		final byte b09x = Integer.valueOf(0x09).byteValue();
		final byte b0Ax = Integer.valueOf(0x0A).byteValue();
		final byte b0Dx = Integer.valueOf(0x0D).byteValue();
		final byte b20x = Integer.valueOf(0x20).byteValue();
		final byte b7Ex = Integer.valueOf(0x7E).byteValue();
		final byte b87x = Integer.valueOf(0x87).byteValue();
		final byte bA1x = Integer.valueOf(0xA1).byteValue();
		final byte bDFx = Integer.valueOf(0xDF).byteValue();
		final byte bEDx = Integer.valueOf(0xED).byteValue();

		String str = String.valueOf(Character.toChars(correctGarbled(ch, CHARSET_W31J)));
		byte[] b = str.getBytes(CHARSET_W31J);
		int size = b.length;
		if (size == 1) {
			// 半角文字
			if (b20x <= b[0] && b[0] <= b7Ex && isValid(b[0], str)) {
				return true;
			}
			if (kana && bA1x <= b[0] && b[0] <= bDFx) {
				return true;
			}
			if (b09x == b[0] || b0Ax == b[0] || b0Dx == b[0]) {
				return true;
			}
		} else if (size == 2) {
			// 全角文字
			return ext || (b87x != b[0] && bEDx > b[0]);
		}

		return false;
	}

	/**
	 * 変換判定処理
	 *
	 * @param b 変換後バイト
	 * @param str 変換前文字列
	 * @return 正しく変換された場合 true
	 */
	private static boolean isValid(final byte b, final String str) {
		final byte b3Fx = Integer.valueOf(0x3F).byteValue();
		if (b != b3Fx) {
			return true;
		}

		byte[] bytes = str.getBytes(StandardCharsets.UTF_16BE);
		return bytes.length == 2 && bytes[0] == 0x00 && bytes[1] == 0x3F;
	}
}
